iplab.png

Social Media Data Analysis - A.A. 2023-2024

Web Scraping


Francesco Ragusa - https://iplab.dmi.unict.it/ragusa/ - francesco.ragusa@unict.it

In questo laboratorio vedremo un esempio di Web Scraping mediante la libreria BeautifulSoup4. In particolare, vedremo che il web scraping consiste in due fasi (che vengono in genere alternate):

  • Esplorare la struttura (DOM) del documento mediante un browser;
  • Scrivere il codice per scaricare le pagine da internet e decodificarle.

In questo laboratorio, mostreremo degli esempi di esplorazione DOM con Google Chrome, ma altri browsers (per esempio Mozilla Firefox) possono essere utilizzati in maniera simile.

Per prima cosa installiamo la libreria mediante il seguente comando: pip install bs4.

1 Analisi del DOM ed Estrazione Automatica

Consideriamo l'ecommerce Amazon (https://www.amazon.it/). Inseriamo nella barra di ricerca: "occhiali da sole". Verrà visualizzata la seguente pagina web:

Screenshot%202023-10-20%20123920.png

In particolare, la pagina mostra la lista degli 'occhiali da sole' in vendita sul sito. Notiamo che per ogni prodotto sono riportate diverse informazioni, quali ad esempio:

  • Il produttore degli occhiali (es. 'Netrox');
  • Il modello degli occhiali (es. 'Occhiali da sole - Occhiali sportivi e vetro a specchio');
  • L'immagine degli occhiali;
  • Il prezzo (es. €14,49);
  • Il numero di recensioni (es. 11);
  • La votazione media ottenuta nelle recensioni (es. 5/5 stelle).

Notiamo inoltre, che la pagina mostra solo una parte dell'elenco (articoli 1-48 di più di 70000) e che in fondo alla pagina sono disponibili dei link per passare alle pagine successive.

In questo laboratorio vedremo come costruire uno script che permette di navigare tra le pagine ed estrarre le informazioni elencate sopra in maniera automatica. Dato che il sito non espone una API, questo processo resta 'semi-manuale'. Sfrutteremo infatti il contenuto HTML. Va notato che, benché HTML è strutturato, esso non è stato pensato per permettere a delle macchine di comunicare tra loro (non è una API!), ma per permettere al browser di visualizzare delle pagine per l'utente finale. Pertanto le strutture HTML delle pagine sono in genere ambigue e poco standard. Per questo motivo sarà necessario esplorare manualmente il DOM caso per caso.

Iniziamo facendo click col tasto destro sulla parte della pagina che contiene l'item e facendo click su 'ispeziona'. Si aprirà un inspector sulla destra (o in basso). Uno degli elementi HTML verrà automaticamente evidenziato. Scorriamo la lista degli elementi HTML nell'inspector e facciamo click su di essi per vedere a quali elementi grafici essi corrispondono. Navigando nel DOM, dovremmo trovare un div contraddistinto da diverse classi, tra cui puis-card-container s-card-container s-overflow-hidden aok-relative puis-expand-height puis-include-content-margin puis puis-vwvhvgkypx2z322f29wcc4lx0s s-latency-cf-section puis-card-border.

Screenshot%202023-10-20%20124453.png

Se scorriamo la struttura del documento nell'inspector, notiamo che la pagina contiene una lista di elementi div di classe puis-card-container s-card-container s-overflow-hidden aok-relative puis-expand-height puis-include-content-margin puis puis-vwvhvgkypx2z322f29wcc4lx0s s-latency-cf-section puis-card-border. Ciascuno di questi div contiene a sua volta gli elementi relativi a ciascun prodotto. Per verificare la nostra intuizione, iniziamo ad analizzare la pagina html mediante BeautifulSoup. Per poterlo fare, dovremo prima scaricare il contenuto grezzo della pagina HTML mediante urllib:

In [146]:
from urllib.request import urlopen as uRequest
uClient=uRequest("https://www.amazon.it/s?k=occhiali+da+sole&__mk_it_IT=%C3%85M%C3%85%C5%BD%C3%95%C3%91&crid=X83F9HUOVIJY&sprefix=occhiali+da+sole%2Caps%2C118&ref=nb_sb_noss_1")
page_html = uClient.read()
print(type(page_html))
<class 'bytes'>

La variabile page_html contiene il contenuto 'grezzo' della pagina. La pagina può essere molto lunga, quindi è in genere sconsigliato provare a stamparla nella sua interezza con una print (ciò può bloccare il programma). Visualizziamo i primi $1000$ caratteri:

In [147]:
print(page_html[:1000])
b'<!doctype html><html lang="it-it" class="a-no-js" data-19ax5a9jf="dingo"><!-- sp:feature:head-start -->\n<head><script>var aPageStart = (new Date()).getTime();</script><meta charset="utf-8"/>\n<!-- sp:end-feature:head-start -->\n<!-- sp:feature:csm:head-open-part1 -->\n\n<!-- sp:end-feature:csm:head-open-part1 -->\n<!-- sp:feature:cs-optimization -->\n<meta http-equiv=\'x-dns-prefetch-control\' content=\'on\'>\n<link rel="dns-prefetch" href="https://images-eu.ssl-images-amazon.com">\n<link rel="dns-prefetch" href="https://m.media-amazon.com">\n<link rel="dns-prefetch" href="https://completion.amazon.com">\n<!-- sp:end-feature:cs-optimization -->\n<!-- sp:feature:csm:head-open-part2 -->\n\n<!-- sp:end-feature:csm:head-open-part2 -->\n<!-- sp:feature:aui-assets -->\n<link rel="stylesheet" href="https://m.media-amazon.com/images/I/11EIQ5IGqaL._RC|01ZTHTZObnL.css,41GU8hNR+SL.css,31Q1jkp0osL.css,013z33uKh2L.css,017DsKjNQJL.css,0131vqwP5UL.css,41EWOOlBJ9L.css,11TIuySqr6L.css,01ElnPiDxWL.css,11fJbvhE5HL.css,01Dm'

Teoricamente, potremmo fare il parsing della pagina in maniera manuale. In pratica, dato che le pagine HTML hanno una struttura generale simile (sono tutte composte da tag organizzati in maniera gerarchica), esistono diverse librerie per semplificare la loro manipolazione. Importiamo BeautifulSoup e processiamo la pagina:

In [148]:
from bs4 import BeautifulSoup as soup
page_soup = soup(page_html)
print(type(page_soup))
<class 'bs4.BeautifulSoup'>

La variabile page_soup è adesso un oggetto di tipo BeautifulSoup. Tale oggetto ha a disposizione una serie di metodi che permettono di manipolare facilmente il documento, come vedremo a breve. Proviamo ad esempio a cercare tutti i div di classe puis-card-container s-card-container s-overflow-hidden aok-relative puis-expand-height puis-include-content-margin puis puis-vwvhvgkypx2z322f29wcc4lx0s s-latency-cf-section puis-card-border per vedere se la nostra intuizione è corretta:

In [149]:
containers=page_soup.findAll('div',{'class':'puis-card-container s-card-container s-overflow-hidden aok-relative puis-expand-height puis-include-content-margin puis puis-vwvhvgkypx2z322f29wcc4lx0s s-latency-cf-section puis-card-border'})
print(type(containers))
<class 'bs4.element.ResultSet'>

La chiamata a findAll ha restituito un oggetto di tipo ResultSet. Vediamo quanti elementi sono contenuti nel set:

In [150]:
print(len(containers))
48

Dato che la pagina contiene esattamente $48$ prodotti, ciò conferma la nostra intuizione che le informazioni di ciascun prodotto sono contenute dentro i div di classe puis-card-container s-card-container s-overflow-hidden aok-relative puis-expand-height puis-include-content-margin puis puis-vwvhvgkypx2z322f29wcc4lx0s s-latency-cf-section puis-card-border. Per avere una ulteriore verifica, proviamo a visualizzare il contenuto del primo container:

In [151]:
container=containers[0]
print(container)
<div class="puis-card-container s-card-container s-overflow-hidden aok-relative puis-expand-height puis-include-content-margin puis puis-vwvhvgkypx2z322f29wcc4lx0s s-latency-cf-section puis-card-border"><div class="a-section a-spacing-base a-text-center"><div class="s-product-image-container aok-relative s-text-center s-image-overlay-grey puis-image-overlay-grey s-padding-left-small s-padding-right-small puis-spacing-small s-height-equalized puis puis-vwvhvgkypx2z322f29wcc4lx0s"><div class="s-image-padding"><span class="rush-component" data-component-type="s-product-image" data-render-id="r2bv8x6u8ugox32vc97e8g45pcp" data-version-id="vwvhvgkypx2z322f29wcc4lx0s"><a class="a-link-normal s-no-outline" href="/LINVO-Occhiali-Polarizzati-Protezione-Design/dp/B09ZQK2QQS"><div class="a-section aok-relative s-image-tall-aspect"><img alt="LINVO Occhiali da Sole Polarizzati per Uomo Donna Protezione alla Guida Protezione UV tile Retrò Anni '80 Design" class="s-image" data-image-index="1" data-image-latency="s-product-image" data-image-load="" data-image-source-density="1" src="https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL320_.jpg" srcset="https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL320_.jpg 1x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL480_QL65_.jpg 1.5x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL640_QL65_.jpg 2x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL800_QL65_.jpg 2.5x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL960_QL65_.jpg 3x"/></div></a></span></div></div><div class="a-section a-spacing-small puis-padding-left-micro puis-padding-right-micro"><div class="a-section a-spacing-none a-text-center"><div class="a-section s-color-swatch-container s-quick-view-text-align-start"><div data-csa-c-content-id="color-swatch-more-link" data-csa-c-interaction-events="click" data-csa-c-swatch-more-url="/LINVO-Occhiali-Polarizzati-Protezione-Design/dp/B09ZQK2QQS/ref=cs_sr_dp" data-csa-c-swatch-remaining-count="+29 colori/motivi" data-csa-c-type="link"><a class="a-link-normal s-color-swatch-link puis-spacing-small s-hidden-in-quick-view" href="/LINVO-Occhiali-Polarizzati-Protezione-Design/dp/B09ZQK2QQS/ref=cs_sr_dp" role="link"><u>+29 colori/motivi</u></a></div></div></div><div class="a-section a-spacing-none a-spacing-top-small s-title-instructions-style"><div class="a-row a-size-base a-color-secondary"><h2 class="a-size-mini s-line-clamp-1"><span class="a-size-base-plus a-color-base">LINVO</span></h2></div><h2 class="a-size-mini a-spacing-none a-color-base s-line-clamp-2"><a class="a-link-normal s-underline-text s-underline-link-text s-link-style a-text-normal" href="/LINVO-Occhiali-Polarizzati-Protezione-Design/dp/B09ZQK2QQS"><span class="a-size-base-plus a-color-base a-text-normal">Occhiali da Sole Polarizzati per Uomo Donna Protezione alla Guida Protezione UV tile Retrò Anni '80 Design</span> </a> </h2></div><div class="a-section a-spacing-none a-spacing-top-micro"><div class="a-row a-size-small"><span aria-label="4,3 su 5 stelle"><span class="a-declarative" data-a-popover='{"position":"triggerBottom","popoverLabel":"","url":"/review/widgets/average-customer-review/popover/ref=acr_search__popover?ie=UTF8&amp;asin=B09ZQK2QQS&amp;ref=acr_search__popover&amp;contextId=search","closeButton":false,"closeButtonLabel":""}' data-action="a-popover" data-csa-c-func-deps="aui-da-a-popover" data-csa-c-type="widget" data-render-id="r2bv8x6u8ugox32vc97e8g45pcp" data-version-id="vwvhvgkypx2z322f29wcc4lx0s"><a class="a-popover-trigger a-declarative" href="javascript:void(0)" role="button"><i class="a-icon a-icon-star-small a-star-small-4-5 aok-align-bottom"><span class="a-icon-alt">4,3 su 5 stelle</span></i><i class="a-icon a-icon-popover"></i></a></span> </span><span aria-label="2.079"><a class="a-link-normal s-underline-text s-underline-link-text s-link-style" href="/LINVO-Occhiali-Polarizzati-Protezione-Design/dp/B09ZQK2QQS#customerReviews"><span class="a-size-base s-underline-text">2.079</span> </a> </span></div><div class="a-row a-size-base"><span class="a-size-base a-color-secondary">400+ acquistati nel mese scorso</span></div></div><div class="a-section a-spacing-none a-spacing-top-small s-price-instructions-style"><div class="a-row a-size-base a-color-base"><a class="a-size-base a-link-normal s-no-hover s-underline-text s-underline-link-text s-link-style a-text-normal" href="/LINVO-Occhiali-Polarizzati-Protezione-Design/dp/B09ZQK2QQS"><span class="a-price" data-a-color="base" data-a-size="xl"><span class="a-offscreen">18,99 €</span><span aria-hidden="true"><span class="a-price-whole">18,99</span><span class="a-price-symbol">€</span></span></span> </a> </div><div class="a-row a-size-base a-color-secondary"><span class="rush-component" data-component-props='{"asin":"B09ZQK2QQS"}' data-component-type="s-coupon-component" data-render-id="r2bv8x6u8ugox32vc97e8g45pcp" data-version-id="vwvhvgkypx2z322f29wcc4lx0s"><span class="s-coupon-clipped aok-hidden"><span class="a-color-base">2,00 € coupon applicato al momento del pagamento</span></span><span class="s-coupon-unclipped"><span class="a-size-base s-highlighted-text-padding aok-inline-block s-coupon-highlight-color">Risparmia 2,00 €</span> <span class="a-color-base"> con coupon (taglie/colori limitati)</span></span></span> </div></div><div class="a-section a-spacing-none a-spacing-top-micro"><div class="a-row a-size-base a-color-secondary s-align-children-center"><div class="a-row"><span aria-label="Consegna GRATUITA lun, 23 ott sul tuo primo ordine idoneo"><span class="a-color-base">Consegna GRATUITA </span><span class="a-color-base a-text-bold">lun, 23 ott </span><span class="a-color-base">sul tuo primo ordine idoneo</span></span></div><div class="a-row"><span aria-label="oppure consegna più rapida domani, 21 ott "><span class="a-color-base">oppure consegna più rapida </span><span class="a-color-base a-text-bold">domani, 21 ott </span></span></div></div></div></div></div></div>
In [152]:
type(container)
Out[152]:
bs4.element.Tag

Se guardiamo attentamente, vedremo del testo compatibile con gli elementi disponibili per ogni prodotto. Ad esempio, possiamo scorgere un LINVO e un €18,99. Possiamo procedere analizzando ricorsivamente il contenuto di ciascun container per estrarre le informazioni che ci servono.

Cerchiamo di estrarre adesso il manufacturer (LINVO, nel caso del primo container). Se ispezioniamo il DOM con il browser (click con il tasto destro sul manufacturer e click su inspect), notiamo che esso è contenuto in uno span di classe a-size-base-plus a-color-base.

Screenshot%202023-10-20%20125217.png

Proviamo ad analizzare il primo container per vedere se contiene uno span di quella classe:

In [155]:
container.findAll('span',{'class':'a-size-base-plus a-color-base'})
Out[155]:
[<span class="a-size-base-plus a-color-base">LINVO</span>]

Abbiamo trovato il manufacturer! Il comando findAll, ci restituisce però una lista di oggetti (uno solo in questo caso). Possiamo accedere al testo contenuto nell'oggetto come segue:

In [156]:
manufacturer = container.findAll('span',{'class':'a-size-base-plus a-color-base'})[0].text
print(manufacturer)
LINVO

Allo stesso modo, ispezionando il DOM, notiamo che il nome del modello è contenuto in uno span di classe a-size-base-plus a-color-base a-text-normal:

Screenshot%202023-10-20%20125325.png

Estraiamo il modello dal container come segue:

In [157]:
model = container.findAll('span',{'class':'a-size-base-plus a-color-base a-text-normal'})[0].text
print(model)
Occhiali da Sole Polarizzati per Uomo Donna Protezione alla Guida Protezione UV tile Retrò Anni '80 Design

Similmente notiamo che le immagini dei prodotti si trovano in un div di classe a-section aok-relative s-image-tall-aspect:

In [158]:
container.findAll('div',{'class':'a-section aok-relative s-image-tall-aspect'})
Out[158]:
[<div class="a-section aok-relative s-image-tall-aspect"><img alt="LINVO Occhiali da Sole Polarizzati per Uomo Donna Protezione alla Guida Protezione UV tile Retrò Anni '80 Design" class="s-image" data-image-index="1" data-image-latency="s-product-image" data-image-load="" data-image-source-density="1" src="https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL320_.jpg" srcset="https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL320_.jpg 1x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL480_QL65_.jpg 1.5x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL640_QL65_.jpg 2x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL800_QL65_.jpg 2.5x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL960_QL65_.jpg 3x"/></div>]

Notiamo che l'immagine si trova dentro il tag img. Possiamo accedere come segue:

In [159]:
container.findAll('div',{'class':'a-section aok-relative s-image-tall-aspect'})[0].img
Out[159]:
<img alt="LINVO Occhiali da Sole Polarizzati per Uomo Donna Protezione alla Guida Protezione UV tile Retrò Anni '80 Design" class="s-image" data-image-index="1" data-image-latency="s-product-image" data-image-load="" data-image-source-density="1" src="https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL320_.jpg" srcset="https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL320_.jpg 1x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL480_QL65_.jpg 1.5x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL640_QL65_.jpg 2x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL800_QL65_.jpg 2.5x, https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL960_QL65_.jpg 3x"/>

La URL della immagine, si trova nell'attributo src, al quale possiamo accedere come segue:

In [160]:
img_url = container.findAll('div',{'class':'a-section aok-relative s-image-tall-aspect'})[0].img['src']
print(img_url)
https://m.media-amazon.com/images/I/41nt78vZ4oL._AC_UL320_.jpg

Possiamo verificare che l'immagine sia corretta inserendo la URL nel browser, o caricandola mediante IPython:

In [161]:
import IPython
IPython.display.Image(img_url)
Out[161]:

In maniera simile (analizzando il DOM manualmente e controllando) possiamo estrarre il prezzo:

In [162]:
price = container.findAll('span',{'class':'a-price-whole'})[0].text
print(price)
18,99

Convertiamo la stringa in un numero sostituendo la virgola con un punto:

In [163]:
price = float(price.replace(',','.'))
print(price)
18.99

In maniera del tutto analoga otteniamo il numero di recensioni:

In [164]:
review_count = container.findAll('span',{'class':'a-size-base s-underline-text'})[0].text
review_count = int(review_count.replace('.', ''))
print(review_count)
2079

Per estrarre il rating, ispezionando il DOM, notiamo che esso è contenuto in uno span di classe a-icon-alt:

Screenshot%202023-10-20%20130007.png

Il rating è espresso in quantità di stelline nel range 0 - 5: 4,3 su 5 stelle. Per ottenere il rating, dobbiamo dunque isolare il primo numero rispetto al resto. Iniziamo cercando i div di classe a-icon-alt:

In [165]:
container.findAll('span',{'class':'a-icon-alt'})
Out[165]:
[<span class="a-icon-alt">4,3 su 5 stelle</span>]
In [166]:
rating = container.findAll('span',{'class':'a-icon-alt'})[0].text
print(rating)
4,3 su 5 stelle

Siamo interessati solo al primo numero del rating, quindi rimuoviamo tutto il resto:

In [167]:
rating = float(rating.replace(' su 5 stelle', '').replace(',', '.'))
print(rating)
4.3

Possiamo automatizzare l'estrazione di queste informazioni in tutta la pagina mediante un ciclo for:

In [186]:
def scrape_page(page_url, _records = None):
    if _records is None:
        _records = []
    page_html=uRequest(page_url).read()
    page_soup=soup(page_html)
    containers=page_soup.findAll('div',{'class':'puis-card-container s-card-container s-overflow-hidden aok-relative puis-expand-height puis-include-content-margin puis puis-vwvhvgkypx2z322f29wcc4lx0s s-latency-cf-section puis-card-border'})
    for container in containers:
        manufacturer = container.findAll('span',{'class':'a-size-base-plus a-color-base'})[0].text
        model = container.findAll('span',{'class':'a-size-base-plus a-color-base a-text-normal'})[0].text
        img_url = container.findAll('div',{'class':'a-section aok-relative s-image-tall-aspect'})[0].img['src']
        try:
            price = container.findAll('span',{'class':'a-price-whole'})[0].text
            price = float(price.replace(',','.'))
        except:
            price = 0
        try:
            review_count = container.findAll('span',{'class':'a-size-base s-underline-text'})[0].text
            review_count = int(review_count.replace('.', ''))
        except:
            review_count = 0
        try:
            rating = container.findAll('span',{'class':'a-icon-alt'})[0].text
            rating = float(rating.replace(' su 5 stelle', '').replace(',', '.'))
        except:
            rating=0
        _records.append([manufacturer, model, img_url, price, review_count, rating])
    
        
    return _records

Adesso facciamo scraping della pagina e inseriamo il risultato in un DataFrame:

In [187]:
import pandas as pd
page_url = 'https://www.amazon.it/s?k=occhiali+da+sole&__mk_it_IT=%C3%85M%C3%85%C5%BD%C3%95%C3%91&crid=X83F9HUOVIJY&sprefix=occhiali+da+sole%2Caps%2C118&ref=nb_sb_noss_1'
records = scrape_page(page_url)
data = pd.DataFrame(records, columns=['manufacturer','model','img_url','price','review_count','rating'])

Visualizziamo le prime righe del DataFrame:

In [188]:
print(data.info())
data.head()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 48 entries, 0 to 47
Data columns (total 6 columns):
manufacturer    48 non-null object
model           48 non-null object
img_url         48 non-null object
price           48 non-null float64
review_count    48 non-null int64
rating          48 non-null float64
dtypes: float64(2), int64(1), object(3)
memory usage: 2.4+ KB
None
Out[188]:
manufacturer model img_url price review_count rating
0 LINVO Occhiali da Sole Polarizzati per Uomo Donna Pr... https://m.media-amazon.com/images/I/41nt78vZ4o... 18.99 2082 4.3
1 Long Keeper Occhiali da Sole Rettangolari Vintage Donna Ae... https://m.media-amazon.com/images/I/51Y2bPPOEK... 13.97 3900 4.2
2 wearPro Occhiali da sole pilota polarizzati per uomini... https://m.media-amazon.com/images/I/51BH+5wngj... 9.99 1080 4.3
3 Hawkers One Occhiali da Sole Uomo https://m.media-amazon.com/images/I/51p-1sQOm7... 27.60 18536 4.4
4 Polaroid Occhiali da Sole Uomo https://m.media-amazon.com/images/I/61j9TzijGC... 29.00 770 4.2

Il DataFrame contiene $48$ righe relative ai $48$ oggetti presenti nella pagina.

2.1 Download delle Immagini

Abbiamo conservato le URL alle immagini nel DataFrame. Tuttavia, vista la natura dinamica dei siti web, queste potrebbero cambiare URL ed essere in futuro irraggiungibili. E' quindi in genere una buona idea scaricare anche questo tipo di dato e inserirlo in una cartella apposita. Iniziamo creando una cartella amazon_img:

In [189]:
import os
dest_dir = 'amazon_img'
os.makedirs(dest_dir, exist_ok=True)

Adesso dobbiamo scaricare ogni immagine e conservarla con un nome di file univoco. Per evitare che immagini diverse abbiano lo stesso nome, derivereremo il nome del file dall'id del DataFrame. Vediamo un esempio di download e salvataggio dell'immagine nel caso in una riga del dataframe:

In [196]:
row = data.iloc[0]
row
Out[196]:
manufacturer                                                LINVO
model           Occhiali da Sole Polarizzati per Uomo Donna Pr...
img_url         https://m.media-amazon.com/images/I/41nt78vZ4o...
price                                                       18.99
review_count                                                 2082
rating                                                        4.3
Name: 0, dtype: object

Deriveremo il nome dell'immagine dall'id della riga (contenuta in name) secondo il seguente formato:

In [59]:
fname = "img_{id:05d}.{ext:s}"

Dove id indica l'id della riga, mentre ext è l'estensione. Per ottenere l'estensione corretta, recuperiamo quella contenuta nell URL mediante una espressione regolare:

In [190]:
import re #libreria per le espressioni regolari
ext = re.search('[^.]+$',row['img_url']).group()
ext
Out[190]:
'jpg'

Il nome del file di destinazione diventa dunque:

In [191]:
fname.format(id=int(row.name),ext=ext)
Out[191]:
'img_00000.jpg'

Il percorso completo del file di destinazione possiamo ottenerlo concatenando il path della cartella e il nome del file mediante la funzione join di os.dir:

In [192]:
from os.path import join
fullpath = join(dest_dir,fname.format(id=int(row.name),ext=ext))
fullpath
Out[192]:
'amazon_img\\img_00000.jpg'

Possiamo dunque scaricare il file mediante urllib come segue:

In [197]:
from urllib.request import urlretrieve as retrieve
retrieve(row['img_url'], fullpath)
Out[197]:
('amazon_img\\img_00000.jpg', <http.client.HTTPMessage at 0x239e29a2208>)

Proviamo a caricare l'immagine con PIL per controllare che sia stata correttamente salvata su disco:

In [198]:
from PIL import Image
Image.open(fullpath)
Out[198]:

Scriviamo adesso una funzione per automatizzare il download delle immagini. Per poter tenere traccia delle immagini che salviamo sul disco, inseriremo un nuovo campo img_path al DataFrame.

In [199]:
def download_images(data, dest_dir, fname="img_{id:05d}.{ext:s}"):
    data=data.copy() #preserva il dataframe originale
    img_paths= []
    data['img_path']=None #crea una nuova colonna
    for i, row in data.iterrows():
        ext = re.search('[^.]+$',row['img_url']).group()
        fullpath = join(dest_dir,fname.format(id=int(row.name),ext=ext))
        retrieve(row['img_url'], fullpath)
        img_paths.append(fullpath)
        
    data['img_path']=img_paths
    return data

Utilizziamo la funzione per scaricare le immagini:

In [200]:
data2 = download_images(data, 'amazon_img')

Visualizziamo le prime righe del nuovo DataFrame:

In [201]:
data2.head()
Out[201]:
manufacturer model img_url price review_count rating img_path
0 LINVO Occhiali da Sole Polarizzati per Uomo Donna Pr... https://m.media-amazon.com/images/I/41nt78vZ4o... 18.99 2082 4.3 amazon_img\img_00000.jpg
1 Long Keeper Occhiali da Sole Rettangolari Vintage Donna Ae... https://m.media-amazon.com/images/I/51Y2bPPOEK... 13.97 3900 4.2 amazon_img\img_00001.jpg
2 wearPro Occhiali da sole pilota polarizzati per uomini... https://m.media-amazon.com/images/I/51BH+5wngj... 9.99 1080 4.3 amazon_img\img_00002.jpg
3 Hawkers One Occhiali da Sole Uomo https://m.media-amazon.com/images/I/51p-1sQOm7... 27.60 18536 4.4 amazon_img\img_00003.jpg
4 Polaroid Occhiali da Sole Uomo https://m.media-amazon.com/images/I/61j9TzijGC... 29.00 770 4.2 amazon_img\img_00004.jpg

Controlliamo il numero di immagini in amazon_img:

In [202]:
from glob import glob
len(glob('amazon_img/*'))
Out[202]:
48

Possiamo comunque controllare la cartella che abbiamo creato e notare che conterrà tutte le immagini dei prodotti:

Screenshot%202023-10-20%20141459.png

2. Navigare Tra le Pagine

Abbiamo visto che la lista dei prodotti si estende su più pagine. Vediamo adesso come navigare tra le pagine per raccogliere le informazioni su tutti i prodotti. Andiamo in fondo alla lista e clicchiamo su uno dei pulsanti per aprire le pagine. Ci accorgiamo che il formato dei link delle pagine è il seguente:

https://www.amazon.it/s?k=occhiali+da+sole&page=2&crid=FY51CBDZSXMD&qid=1697798329&sprefix=%2Caps%2C106&ref=sr_pg_2

Sfrutteremo questa caratteristica per navigare tra le pagine.

Dato un numero di pagina, possiamo trovare il link relativo con il formato:

In [203]:
p=1
f"https://www.amazon.it/s?k=occhiali+da+sole&page={p:d}&crid=FY51CBDZSXMD&qid=1697798329&sprefix=%2Caps%2C106&ref=sr_pg_2"
Out[203]:
'https://www.amazon.it/s?k=occhiali+da+sole&page=1&crid=FY51CBDZSXMD&qid=1697798329&sprefix=%2Caps%2C106&ref=sr_pg_2'

Per rendere il codice indipendente rispetto alla pagina:

In [204]:
part1_url = "https://www.amazon.it/s?k=occhiali+da+sole&page="
part2_url = "&crid=FY51CBDZSXMD&qid=1697798329&sprefix=%2Caps%2C106&ref=sr_pg_2"
f"{part1_url}{p:d}{part2_url}"
Out[204]:
'https://www.amazon.it/s?k=occhiali+da+sole&page=1&crid=FY51CBDZSXMD&qid=1697798329&sprefix=%2Caps%2C106&ref=sr_pg_2'

Rivediamo lo script precedente per automatizzare la navigazione tra le pagine e lo scraping:

In [205]:
def navigate_and_scrape(base_url, records = None):
    if records is None:
        records = []
    
    all_records = records
    page = 1 #iniziamo dalla pagina 1
    while(True):
        url = f"{part1_url}{p:d}{part2_url}"
        records = scrape_page(url)
        all_records.extend(records)
        if len(records)==0: #usciamo quando la pagina non contiene più record
            break
        page+=1
    
    return all_records

Utilizziamo la funzione per effettuare lo scraping. Inviando tutte queste richieste in successione, Amazon bloccherà l'accesso mostrando l'errore: HTTP Error 503: Service Unavailable:

Screenshot%202023-10-20%20142039.png

Modifichiamo lo script in modo da fare scraping solo delle prime 3 pagine:

In [206]:
def navigate_and_scrape(base_url, records = None):
    if records is None:
        records = []
    
    all_records = records
    page = 1 #iniziamo dalla pagina 1
    page_end = 3 #fermiamoci alle pagina 3
    while(page  <= page_end):
        url = f"{part1_url}{p:d}{part2_url}"
        records = scrape_page(url)
        all_records.extend(records)
        if len(records)==0: #usciamo quando la pagina non contiene più record
            break
        page+=1
    
    return all_records

Utilizziamo la funzione modificata per effettuare lo scraping:

In [207]:
records = navigate_and_scrape(page_url)
data = pd.DataFrame(records, columns=['manufacturer','model','img_url','price','review_count','rating'])

Visualizziamo alcune informazioni sul DataFrame:

In [208]:
print(data.info())
data.head()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 144 entries, 0 to 143
Data columns (total 6 columns):
manufacturer    144 non-null object
model           144 non-null object
img_url         144 non-null object
price           144 non-null float64
review_count    144 non-null int64
rating          144 non-null float64
dtypes: float64(2), int64(1), object(3)
memory usage: 6.9+ KB
None
Out[208]:
manufacturer model img_url price review_count rating
0 LINVO Occhiali da Sole Polarizzati per Uomo Donna Pr... https://m.media-amazon.com/images/I/41nt78vZ4o... 18.99 2082 4.3
1 Long Keeper Occhiali da Sole Rettangolari Vintage Donna Ae... https://m.media-amazon.com/images/I/51Y2bPPOEK... 13.97 3902 4.2
2 wearPro Occhiali da sole pilota polarizzati per uomini... https://m.media-amazon.com/images/I/51BH+5wngj... 9.99 1081 4.3
3 Hawkers One Occhiali da Sole Uomo https://m.media-amazon.com/images/I/51p-1sQOm7... 27.60 18542 4.4
4 Polaroid Occhiali da Sole Uomo https://m.media-amazon.com/images/I/61j9TzijGC... 29.00 770 4.2

3. Analisi dei Dati Ottenuti

Vediamo adesso di analizzare in breve i dati ottenuti per capire qualcosa di più sui prodotti venduti dallo store. Iniziamo con delle semplici statistiche sulle variabili quantitative, che possono essere ottenute mediante il metodo describe dei DataFrame:

In [209]:
data.describe()
Out[209]:
price review_count rating
count 144.00000 144.000000 144.000000
mean 25.35125 2558.604167 4.233333
std 10.71631 6708.716341 0.636094
min 0.00000 0.000000 0.000000
25% 18.00000 285.750000 4.275000
50% 25.27000 709.000000 4.300000
75% 30.76500 2098.500000 4.400000
max 56.00000 43600.000000 4.600000

Vediamo adesso quanti prodotti sono contenuti per ciascuna marca:

In [210]:
from matplotlib import pyplot as plt
plt.figure(figsize=(8,8))
data.groupby('manufacturer')['manufacturer'].count().plot.pie(rotatelabels=True)
plt.show()

Notiamo che le marche che contengono più oggetti sono Polaroid, Vans e Hawkers. Calcoliamo e visualizziamo adesso il prezzo medio per marca. Questa volta però utilizziamo un grafico a barre:

In [211]:
plt.figure(figsize=(12,6))
data.groupby('manufacturer')['price'].mean().plot.bar()
plt.grid()
plt.show()

Gli oggetti più costosi in media sono gli occhiali "Police" e "Tommy Hilfiger". Cerchiamo di capire adesso quali sono gli occhiali più popolari. Inizieremo visualizzando il numero di review presenti in ogni categoria:

In [212]:
plt.figure(figsize=(12,6))
data.groupby('manufacturer')['review_count'].sum().plot.bar()
plt.grid()
plt.show()

Domanda 1 Confrontando l'ultimo grafico con i precedenti, che tipo di correlazioni sono presenti?

Risposta 1







Pare che gli oggetti più recensiti siano gli occhiali "SUNGAIT" e "Hawkers". Vediamo adesso qual è il rating medio degli oggetti in ogni categoria:

In [213]:
plt.figure(figsize=(12,6))
data.groupby('manufacturer')['rating'].mean().plot.bar()
plt.grid()
plt.show()

Gli oggetti che hanno i rating più bassi sono quelli appartenenti alla marca "ZENOTTIC" (non considerando i "Police" visto che non hanno recensioni).Proviamo a vedere perché. Analizziamo solo quella categoria, mostrando il rating medio per marca:

In [214]:
plt.figure(figsize=(12,6))
data[data['manufacturer']=='ZENOTTIC'].groupby('manufacturer')['rating'].mean().plot.bar()
plt.grid()
plt.show()

Domanda 2 Stando a quanto mostrato nell'ultimo grafico, che tipo di correlazione c'è tra il rating e il numero di recensioni?

Risposta 2







Esercizi

Esercizio 1

Si scarichino tutte le immagini relative all'ultimo DataFrame creato.

Esercizio 2

Esistono ulteriori attributi relativi agli oggetti presi in considerazione come ad esempio l'etichetta "Prime" e anche i tempi di spedizione. Provare ad estrarre anche quelle informazioni e rieffettuare l'analisi statistica considerando anche i nuovi attributi.

Esercizio 3

Si scelga un altro sito a piacere e si ripeta su di esso una analisi simile a quella vista in laboratorio.

Referenze